﻿namespace Filters.Demo.MetricSpaces
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using AForge.Math;
    using Filters.Demo.DataAccess;

    public class MetricSpacesHelper : IMetricSpacesHelper
    {
        public IImageCatalogRepository Repository { get; set; }

        public static double JensenShanonDistance(Complex[] complex1, Complex[] complex2)
        {
            var distanceRe = 0.0;
            for (var i = 0; i < complex1.Count(); i++)
            {
                var denom = complex1[i].Re + complex2[i].Re;
                var firstTerm = 0.0;
                var secondTerm = 0.0;
                if (denom != 0)
                {
                    firstTerm = complex1[i].Re * Math.Log(Math.Abs((2 * complex1[i].Re) / denom));
                    secondTerm = complex2[i].Re * Math.Log(Math.Abs((2 * complex2[i].Re) / denom));
                }

                distanceRe += firstTerm + secondTerm;
                System.Console.WriteLine(distanceRe.ToString());
            }

            var distanceIm = 0.0;
            for (var i = 0; i < complex1.Count(); i++)
            {
                var denom = complex1[i].Im + complex2[i].Im;
                var firstTerm = 0.0;
                var secondTerm = 0.0;
                if (denom != 0)
                {
                    firstTerm = complex1[i].Im * Math.Log(Math.Abs((2 * complex1[i].Im) / (complex1[i].Im + complex2[i].Im)));
                    secondTerm = complex2[i].Im * Math.Log(Math.Abs((2 * complex2[i].Im) / (complex1[i].Im + complex2[i].Im)));
                }

                distanceIm += firstTerm + secondTerm;
            }

            return (distanceRe + distanceIm) / 2;
        }

        public static double EuclideanDistance(Complex[] complex1, Complex[] complex2)
        {
            var distanceRe = 0.0;
            for (var i = 0; i < complex1.Count(); i++)
            {
                distanceRe += Math.Pow((complex1[i].Re - complex2[i].Re), 2.0);
            }

            distanceRe = Math.Sqrt(distanceRe);

            var distanceIm = 0.0;
            for (var i = 0; i < complex1.Count(); i++)
            {
                distanceIm += Math.Pow((complex1[i].Im - complex2[i].Im), 2.0);
            }

            distanceIm = Math.Sqrt(distanceIm);

            return (distanceRe + distanceIm) / 2;
        }

        public void SelectNewPivots(double distanceCoefficient)
        {
            if (distanceCoefficient <= 0)
            {
                throw new ArgumentException("The distanceCoefficient should be greater than zero", "distanceCoefficient");
            }

            if (distanceCoefficient >= 1)
            {
                throw new ArgumentException("The distanceCoefficient should be lower than one", "distanceCoefficient");
            }

            // Clear the previos pivots in the database
            this.Repository.ClearPivots();

            // Get all the images in the database
            var imageModels = this.Repository.GetImageModels();

            // Select the pivots from the available image (using the max distance as a parameter)
            var pivots = SelectPivots(imageModels, CalculateMaxDistance(imageModels) * distanceCoefficient);

            // Save the new pivots in the database. This method also sets to pivot models the IDs generated by the database
            this.Repository.CreatePivots(pivots);

            // Update the DistanceToPivot and the PivotID properties in all the image models
            UpdatePivotInformation(imageModels, pivots);

            // Update the new pivot information in the database
            this.Repository.UpdatePivotInformation(imageModels);
        }

        public void SetPivotInformation(ImageModel imageModel)
        {
            SetPivotInformation(imageModel, this.Repository.GetPivots());
        }

        public IList<ImageModel> GetSimilarImages(Complex[] signature, double maxDistance)
        {
            var pivots = new Dictionary<PivotModel, double>();
            foreach (var pivot in this.Repository.GetPivots())
            {
                var distance = EuclideanDistance(pivot.ImageModel.ImageSignature, signature);
                if (distance <= maxDistance)
                {
                    pivots.Add(pivot, distance);
                }
            }

            var closerImageModels = new List<ImageModel>();
            foreach (var pivot in pivots.Keys)
            {
                foreach (var imageModel in this.Repository.GetCluster(pivot.PivotID))
                {
                    // Triangle inequality
                    imageModel.DistanceToTarget = imageModel.DistanceToPivot + pivots[pivot];
                    if (imageModel.DistanceToTarget <= maxDistance)
                    {
                        closerImageModels.Add(imageModel);
                    }
                }
            }

            return closerImageModels;
        }

        private static IEnumerable<PivotModel> SelectPivots(IEnumerable<ImageModel> imageModels, double maxDistance)
        {
            var pivots = new List<ImageModel> { imageModels.First() };
            foreach (var imageModel in imageModels)
            {
                var isPivot = true;
                foreach (var pivot in pivots)
                {
                    var distance = (pivot == imageModel) ? 0 : EuclideanDistance(imageModel.ImageSignature, pivot.ImageSignature);                       
                    if (distance <= maxDistance)
                    {
                        isPivot = false;
                        break;
                    }
                }

                if (isPivot)
                {
                    pivots.Add(imageModel);
                }
            }

            return pivots
                .Select(im => new PivotModel { ImageModel = im })
                .ToList();
        }

        private static void UpdatePivotInformation(IEnumerable<ImageModel> imageModels, IEnumerable<PivotModel> pivotModels)
        {
            foreach (var imageModel in imageModels)
            {
                SetPivotInformation(imageModel, pivotModels);
            }
        }

        private static double CalculateMaxDistance(IEnumerable<ImageModel> imageModels)
        {
            var maxDistance = 0d;
            foreach (var imageModel1 in imageModels)
            {
                foreach (var imageModel2 in imageModels)
                {
                    if (imageModel1 != imageModel2)
                    {
                        var distance = EuclideanDistance(imageModel1.ImageSignature, imageModel2.ImageSignature);
                        if (distance > maxDistance)
                        {
                            maxDistance = distance;
                        }
                    }
                }
            }

            return maxDistance;
        }

        private static void SetPivotInformation(ImageModel imageModel, IEnumerable<PivotModel> pivotModels)
        {
            if ((pivotModels != null) && (pivotModels.Count() > 0))
            {
                var minDistance = double.MaxValue;
                var pivotId = 0;
                foreach (var pivotModel in pivotModels)
                {
                    var distance = (pivotModel.ImageModel == imageModel) ? 0 : EuclideanDistance(imageModel.ImageSignature, pivotModel.ImageModel.ImageSignature);
                    if (distance < minDistance)
                    {
                        minDistance = distance;
                        pivotId = pivotModel.PivotID;
                    }
                }

                imageModel.DistanceToPivot = minDistance;
                imageModel.PivotID = pivotId;
            }
        }
    }
}
